package gae.testjig.server;

import gae.testjig.client.ClientChannelId;
import gae.testjig.client.ClientTestStats;
import gae.testjig.client.GaeTestJigService;
import gae.testjig.dao.DaoException;
import gae.testjig.data.BizLogicContacts;
import gae.testjig.data.BizLogicOrganizations;
import gae.testjig.data.PMF;
import gae.testjig.dto.Contact;
import gae.testjig.dto.ContactGroup;
import gae.testjig.dto.Counter;
import gae.testjig.dto.Member;
import gae.testjig.dto.Organization;
import gae.testjig.dto.StateMachine;
import gae.testjig.ofy.dao.ODaoSeats;
import gae.testjig.task.Task;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Random;

import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import com.google.appengine.api.channel.ChannelMessage;
import com.google.appengine.api.channel.ChannelService;
import com.google.appengine.api.channel.ChannelServiceFactory;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Transaction;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

/**
 * The server side implementation of the RPC service.
 */
@SuppressWarnings("serial")
public class GaeTestJigServiceImpl extends RemoteServiceServlet implements GaeTestJigService {

	@Override
	public String initiateChannelOpen(ClientChannelId clientChannelId) {
		System.out.println("initiateChannelOpen: clientId="+clientChannelId.getClientId());
		ChannelService channelService = ChannelServiceFactory.getChannelService();
		String token = channelService.createChannel(clientChannelId.getClientId());
		return token;
	}

	@Override
	public void sendEchoChannel(ClientChannelId clientChannelId) {
		System.out.println("sendEchoChannel: clientId="+clientChannelId.getClientId());
		ChannelService channelService = ChannelServiceFactory.getChannelService();
		channelService.sendMessage(new ChannelMessage(clientChannelId.getClientId(), "{\"channelId\": \""+clientChannelId.getChannelId()+"\", \"echo\": \"echo.\"}"));
	}

	@Override
	public ClientTestStats getTestStatus(String testId) {
		PersistenceManager pm = PMF.get().getPersistenceManager();

		Query query = pm.newQuery(Counter.class);
		query.setFilter("testId == testIdParam");
		query.declareParameters("String testIdParam");
		@SuppressWarnings("unchecked")
		List<Counter> results = (List<Counter>) query.execute(testId);
		if ( results.size() != 1 ) {
			throw new RuntimeException("Wrong number of stats found.");
		}
		Counter counter = results.get(0);

		query = pm.newQuery(StateMachine.class);
		query.setFilter("testId == testIdParam");
		query.declareParameters("String testIdParam");
		@SuppressWarnings("unchecked")
		List<StateMachine> results2 = (List<StateMachine>) query.execute(testId);
		if ( results2.size() != 1 ) {
			throw new RuntimeException("Wrong number of state machines found.");
		}
		StateMachine stateMachine = results2.get(0);

		ClientTestStats stats = new ClientTestStats();
		stats.setInvocationCount(counter.getInvocationCount());
		stats.setRetryCount(counter.getRetryCount());
		stats.setState(stateMachine.getState());
		stats.setTestId(testId);
		return stats;
	}

	@Override
	public String startTest1(ClientChannelId clientChannelId) {
		return startTest(false, clientChannelId);
	}

	@Override
	public String startTest2(ClientChannelId clientChannelId) {
		return startTest(true, clientChannelId);
	}
	
	@Override
	public String startTest3(ClientChannelId clientChannelId) {
		String testId = "" + System.currentTimeMillis();

		DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
		Task taskBuilder = Task.BUILD;
		
		Transaction txn = datastore.beginTransaction();
		try {
			Entity rootEntity = new Entity("Fuzzbar");
			rootEntity.setProperty("testId", testId);
			datastore.put(rootEntity);
			
			Entity counterEntity = new Entity("Counter", rootEntity.getKey());
			counterEntity.setProperty("testId", testId);
			counterEntity.setProperty("invocationCount", new Long(0));
			counterEntity.setProperty("retryCount", new Long(0));
			datastore.put(counterEntity);

			Entity stateMachineEntity = new Entity("StateMachine", rootEntity.getKey());
			stateMachineEntity.setProperty("testId", testId);
			stateMachineEntity.setProperty("state", new Long(0));
			stateMachineEntity.setProperty("counterId", counterEntity.getKey());
			datastore.put(stateMachineEntity);
			
			// queue the task to advance to the next state.
			String stateMachineKeyString = KeyFactory.keyToString(stateMachineEntity.getKey());
			String counterKeyString = KeyFactory.keyToString(counterEntity.getKey());
			int expectedState = 0;
			Task.TX_TEST_QUEUE.add(txn, taskBuilder.advanceStateDS(clientChannelId, stateMachineKeyString, counterKeyString, expectedState, 0));
				
			txn.commit();
			
			return testId;
		}
		finally {
			if (txn.isActive()) {
		        txn.rollback();
		    }
		}
	}

	private String startTest(boolean isDelay, ClientChannelId clientChannelId) {
		String testId = "" + System.currentTimeMillis();
		
		PersistenceManager pm;
		Task taskBuilder;
		
		if ( isDelay ) {
			pm = PMF.getForTest2().getPersistenceManager();
			taskBuilder = Task.DELAY2;
		} else {
			pm = PMF.getForTest1().getPersistenceManager();
			taskBuilder = Task.BUILD;
		}
		
		pm.currentTransaction().begin();
		try {
			Counter counter = new Counter();
			counter.setTestId(testId);
			counter.setInvocationCount(0);
			counter.setRetryCount(0);
			pm.makePersistent(counter);

			StateMachine stateMachine = new StateMachine();
			stateMachine.setTestId(testId);
			stateMachine.setState(0);
			stateMachine.setCounterId(counter.getCounterId());
			pm.makePersistent(stateMachine);
			
			// queue the task to advance to the next state.
			String stateMachineKeyString = KeyFactory.keyToString(stateMachine.getStateMachineId());
			String counterKeyString = KeyFactory.keyToString(counter.getCounterId());
			int expectedState = 0;
			Task.TX_TEST_QUEUE.add(taskBuilder.advanceState(clientChannelId, stateMachineKeyString, counterKeyString, expectedState, isDelay?1:0));
				
			pm.currentTransaction().commit();
			
			return testId;
		}
		finally {
			if (pm.currentTransaction().isActive()) {
		        pm.currentTransaction().rollback();
		    }
			pm.close();
		}
	}

	@Override
	public void jdoTest1(ClientChannelId clientChannelId) {
		final String ORG_NAME = "foobar, inc.";
		final String M1_NAME = "fred";
		final String M2_NAME = "george";

		ChannelLogger logger = new ChannelLogger(clientChannelId);
		logger.info("starting test 1.");

		PersistenceManager pm = PMF.get().getPersistenceManager();
		Key orgKey = KeyFactory.createKey(Organization.class.getSimpleName(), "org1");
		Organization org = new Organization();
		org.setOrganizationKey(orgKey);
		org.setName(ORG_NAME);
		pm.makePersistent(org);
		logger.info("created and persisted an Organization object 'org'.");
		logger.info("orgKey    ="+KeyFactory.keyToString(orgKey));
		
		Member m1 = new Member();
		Key memberKey1 = KeyFactory.createKey(orgKey, Member.class.getSimpleName(), "member_key"); // the "local" key
		logger.info("creating a Member object 'm1' and contructing its Key manually.");
		logger.info("memberKey1="+KeyFactory.keyToString(memberKey1));
		m1.setMemberKey(memberKey1);
		m1.setName(M1_NAME);
		pm.makePersistent(m1);
		logger.info("persisted 'm1'.");

		Member m2 = new Member();
		Key memberKey2 = KeyFactory.createKey(orgKey, Member.class.getSimpleName(), "member_key"); // try to fail because it is the same key
		logger.info("creating a Member object 'm2' and contructing its Key manually, intentionally constructing the same key as 'm1'.");
		logger.info("memberKey2="+KeyFactory.keyToString(memberKey2));
		m2.setMemberKey(memberKey2);
		m2.setName(M2_NAME);
		pm.makePersistent(m2); // doesn't fail, just overwrites with same key
		logger.info("persisted 'm2'. It doesn't fail. It just overwrites with the same key.");

		logger.info("Going to lookup our 'org' object by its key using pm.getObjectById().");
		Organization o22 = (Organization) pm.getObjectById(Organization.class, orgKey);
		logger.info("expecting an org name of '"+ORG_NAME+"', and it is: "+o22.getName());

		logger.info("Going to lookup our 'm2' object by its key using pm.getObjectById().");
		Member m22 = (Member) pm.getObjectById(Member.class, memberKey2);
		logger.info("expecting a member name of '"+M2_NAME+"', and it is: "+m22.getName());

		logger.info("Going to delete our 'org' object with pm.deletePersistent().");
		pm.deletePersistent(org);
		logger.info("called pm.deletePersistent(), but the object is still around until we call pm.close(). could still retrieve it by its key.");
		pm.close(); // the delete doesn't really "commit" until close(). you could still retrieve it by key.
		logger.info("called pm.close(). now its gone.");

		logger.info("creating a new PersistenceManager 'pm' since we just closed the last one.");
		pm = PMF.get().getPersistenceManager();
		try {
			logger.info("Going to lookup our 'org' object again by its key using pm.getObjectById(), although we deleted it.");			
			Organization o22x = (Organization) pm.getObjectById(Organization.class, orgKey);
			logger.info("expecting an org name of '"+ORG_NAME+"', and it is: "+o22x.getName());
//			Member m22x = (Member) pm.getObjectById(Member.class, memberKey2);
//			System.out.println("m22x-name="+m22x.getName());
		}
		catch ( JDOObjectNotFoundException e ) {
			logger.info("org not found, as expected.");
		}

		logger.info("creating a non-existant Member key.");
		Key m3Key= KeyFactory.createKey(orgKey, Member.class.getSimpleName(), "non_exist");
		logger.info("memberKey3="+KeyFactory.keyToString(m3Key));
		try {
			logger.info("Going to attempt to lookup our non-existant 'm3' object with its key using pm.getObjectById().");
			Member m3 = (Member) pm.getObjectById(Member.class, m3Key);
			logger.info("not expecting to get here. 'm3' name="+m3.getName());
		}
		catch ( JDOObjectNotFoundException e ) {
			logger.info("member not found, as expected.");
		}
		
		logger.info("test 1 complete.");
		
		/*
		 * what I learned:
		 * 
		 * You can create your own keys manually in JDO, just like you would in pure datastore calls -- you
		 * still just use the KeyFactory to create your key, supplying the partent key if you wish to keep them
		 * in the same chunk.
		 * 
		 * If you create a key identical to an existing entity's key, and call makePersistent() with a new entity,
		 * it will just overwrite any other entity previously living at that key. If you want to test for previous
		 * existence, you have to do that manually. You have to do a getObjectById() and catch JDOObjectNotFoundException.
		 * 
		 * You can delete a parent entity, and its children entities stay intact in the datastore. (as docs suggest)
		 * 
		 * When you do a deletePersistent() call, on an entity, you actually can still recall it from the datastore
		 * with a getObjectById() call. But if you try to read any of its attributes, it will throw an exception
		 * saying you can't read a deleted object. This is significant, because you cannot use getObjectById() to
		 * search for pre-existing entity right after deletPersisent().  Instead, you must call pm.close() in order
		 * to complete the deletion of the entity. Then (in a new pm instance) you can call getObjectById() and get an
		 * exception if it doesn't exist.
		 * 
		 */

	}

	@Override
	public void jdoTest2(ClientChannelId clientChannelId) {
		long ts = System.currentTimeMillis();
		Random R = new Random();
		ChannelLogger logger = new ChannelLogger(clientChannelId);
		logger.info("starting test 2.");
		
		HashMap<Key, String> groupMap = new HashMap<Key, String>();
		
		try {
			logger.info("creating an organization.");
			Organization org = new BizLogicOrganizations().createOrganization("FizzBar, Inc.");
			BizLogicContacts biz = new BizLogicContacts(org.getOrganizationKey());
			logger.info("done. now creating some groups for our organization.");
			String[] groupNames = {"Purple", "Orange", "Blue", "Yellow", "Red", "Green", "Black", "White" };
			ArrayList<ContactGroup> groups = new ArrayList<ContactGroup>();
			for ( String name : groupNames ) {
				logger.info(" creating group: "+name);
				ContactGroup cg = biz.createContactGroup(name);
				groups.add(cg);
				groupMap.put(cg.getContactGroupKey(), cg.getName());
			}
			logger.info("groups created.");
			
			logger.info("creating 25 random contacts with random group assignment.");
			ArrayList<Contact> contacts = new ArrayList<Contact>();
			for (int i=0;i<25;i++) {
				int index = R.nextInt(1+groupNames.length) - 1;
				if ( index < 0 ) {
					contacts.add(biz.createRandomContact(null));	
				} else {
					contacts.add(biz.createRandomContact(groups.get(index).getContactGroupKeyString()));
				}
			}
			
			logger.info("making some random group assignments, a block at a time");
			for (int i=0;i<4;i++) {
				doBlockGroupAssignment(logger, R, biz, groups, contacts);
			}
			
			// do Audit at this point
			doAudit(logger, biz, contacts, groupMap);
			
			logger.info("delete groups one at a time");
			for ( ContactGroup cg : groups ) {
				logger.info("deleting "+cg.getName());
				biz.deleteGroup(cg.getContactGroupKeyString());
			}
			
			logger.info("do another audit and see if our groups are clean...");
			doAudit(logger, biz, contacts, groupMap);
			
			logger.info("deleting our contacts");
			ArrayList<Key> contactKeys = new ArrayList<Key>();
			for ( Contact c : contacts ) {
				contactKeys.add(c.getContactKey());
			}
			biz.deleteContacts(contactKeys);

			logger.info("deleting our organization");
			new BizLogicOrganizations().deleteOrganization(org.getOrganizationKey());
			
			logger.info("completed test 2.");
		}
		catch (DaoException e) {
			logger.info("test aborted: " + e.getMessage());
		}
		
		long elapsed = System.currentTimeMillis() - ts;
		System.out.println("elapsed="+elapsed+"ms");
		logger.info("real elasped test time = "+elapsed+"ms");
	}

	private void doAudit(ChannelLogger logger, BizLogicContacts biz, ArrayList<Contact> contacts, HashMap<Key, String> groupMap) throws DaoException {
		logger.info("auditing the state of our data");
		contacts = biz.getAllContacts();
		for ( Contact c : contacts ) {
			StringBuilder buf = new StringBuilder();
			buf.append(" ");
			buf.append(c.getName()).append(" ");
			buf.append(c.getPhoneNumber()).append(" [ ");
			for ( Key k : c.getContactGroups() ) {
				buf.append(groupMap.get(k)).append(" ");
			}
			buf.append("]");
			logger.info(buf.toString());
		}
	}

	private void doBlockGroupAssignment(ChannelLogger logger, Random R, BizLogicContacts biz, ArrayList<ContactGroup> groups, ArrayList<Contact> contacts) throws DaoException {
		int pick = R.nextInt(groups.size());
		logger.info("selecting 6 random contacts, and adding them to group "+groups.get(pick).getName());
		ArrayList<Key> contactKeys = new ArrayList<Key>();
		for (int i=0;i<6;i++) {
			contactKeys.add(contacts.get(R.nextInt(contacts.size())).getContactKey());
		}
		biz.updateGroupMembership(contactKeys, groups.get(pick).getContactGroupKeyString(), true);
	}


	@Override
	public void resetConstraintTest(ClientChannelId ccid, String[] ownerNames, String seatId) {
		ChannelLogger logger = new ChannelLogger(ccid);
		logger.info("Resetting the constraint test...");
		for ( String name : ownerNames ) {
			MemcacheSemaphore ms = new MemcacheSemaphore(name+seatId);
			ms.abort();
			ms.setUnderway(false);
		}
		
		// cleanup prior seat reservation.
		ConstraintTest ct = new ConstraintTest();
		ct.deleteSeatEntity(seatId);
		
		// cleanup the objectify items, if any
		ODaoSeats.deleteSeatEntity(seatId);
		
		// let the abort signals expire
		try { Thread.sleep(2100); } catch (InterruptedException e) {}
		logger.info("The constraint test has been reset.");
		
		ChannelService channelService = ChannelServiceFactory.getChannelService();
		channelService.sendMessage(new ChannelMessage(ccid.getClientId(), "{\"channelId\": \""+ccid.getChannelId()+"\", \"ready\": \"reset\"}"));

	}
	
	@Override
	public boolean doConstraintTest(ClientChannelId ccid, String ownerName, String seatId) {
		MemcacheSemaphore ms = new MemcacheSemaphore(ownerName+seatId);
		if ( ms.isUnderway() ) {
			// just send an advance signal
			return ms.advance();
		} else {
			// get it started
			Task.TX_TEST_QUEUE.add(Task.BUILD.constraintTest(ccid, ownerName, seatId));
			return true;
		}
	}

	@Override
	public void fetchSeatInfo(ClientChannelId ccid, String seatId) {
		ConstraintTest ct = new ConstraintTest();
		String info = ct.fetchSeatInfo(seatId);
		ChannelService channelService = ChannelServiceFactory.getChannelService();
		channelService.sendMessage(new ChannelMessage(ccid.getClientId(), "{\"channelId\": \""+ccid.getChannelId()+"\", \"info\": \""+info+"\"}"));
	}

	@Override
	public void doRace(ClientChannelId ccid, String[] ownerName, String seatId) {
		long runAtTimestamp = System.currentTimeMillis() + 1000; // run these a second in the future
		int offset = (int) (runAtTimestamp % ownerName.length); // randomly pick who gets queued first
		ChannelLogger logger = new ChannelLogger(ccid);
		logger.info("Starting race to reserve seat "+seatId+".");
		for (int i=offset;i<(ownerName.length+offset);i++) {
			int index = i % ownerName.length;
			String name = ownerName[index];
			Task.TX_TEST_QUEUE.add(Task.BUILD.constraintTestRace(ccid, name, seatId, runAtTimestamp));
		}
	}

	@Override
	public void doRaceObjectify(ClientChannelId ccid, String[] ownerName, String seatId) {
		long runAtTimestamp = System.currentTimeMillis() + 1000; // run these a second in the future
		int offset = (int) (runAtTimestamp % ownerName.length); // randomly pick who gets queued first
		ChannelLogger logger = new ChannelLogger(ccid);
		logger.info("Starting race to reserve seat "+seatId+" (and using objectify).");
		for (int i=offset;i<(ownerName.length+offset);i++) {
			int index = i % ownerName.length;
			String name = ownerName[index];
			Task.TX_TEST_QUEUE.add(Task.BUILD.constraintTestRaceOfy(ccid, name, seatId, runAtTimestamp));
		}
	}

}
